package com.adee.java.classloader;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;

/*
类加载过程：
1.加载
    根据类全限定名读取二进制流到方法区，并按照jvm的格式保存在方法区，然后在堆中创建对应的Class对象，
    之后代码就可以通过操作Class对象来控制方法区中类的结构了。
    此阶段可控性很强，可使用系统类加载器，也可使用自定义类加载器。
2.验证
    检验被加载的类的正确性和安全性。
    分为：文件格式验证、元数据验证、字节码验证、符号引用验证。
3.准备
    为类的静态变量分配内存（在方法区中分配），并将其初始化为默认值（0或null）或常量值（static final）。
4.解析
    虚拟机将常量池中的符号引用替换为直接引用。
5.初始化（类）
    初始化，为类的静态变量赋予正确的初始值，JVM负责对类进行初始化，主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式：
    ① 声明类变量是指定初始值。
    ② 使用静态代码块为类变量指定初始值。
    JVM类初始化步骤：
    1、假如这个类还没有被加载和连接，则程序先加载并连接该类
    2、假如该类的直接父类还没有被初始化，则先初始化其直接父类
    3、假如类中有初始化语句，则系统依次执行这些初始化语句
    类初始化时机：只有当对类的主动使用的时候才会导致类的初始化，类的主动使用包括以下六种：

    • 创建类的实例，也就是new的方式
    • 访问某个类或接口的静态变量，或者对该静态变量赋值
    • 调用类的静态方法
    • 反射（如Class.forName(“com.shengsiyuan.Test”)）
    • 初始化某个类的子类，则其父类也会被初始化
    • Java虚拟机启动时被标明为启动类的类（Java Test），直接使用java.exe命令来运行某个主类

双亲委派模型：
1.AppClassLoader加载类，委派给ExtClassLoader，ExtClassLoader又委派给BootStrapClassLoader；
2.若BootStrapClassLoader加载失败（例如在$JAVA_HOME/jre/lib中未找到该class），则ExtClassLoader加载；
3.若ExtClassLoader加载失败，则AppClassLoader加载；
4.若AppClassLoader也加载失败，则会抛出ClassNotFoundException异常。

一般情况下，自定义类加载器用于加载处于jre/lib、jre/ext/lib、classpath之外的类。
 */
public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
//         test1();
//        test4();
//        test3();
        test4();
    }
    // 测试使用自定义类加载器加载的类中，该类方法中创建的对象是由哪个类加载器加载的
    public static void test4() throws Exception{
        MyClassLoader loader1 = new MyClassLoader(ClassLoaderTest.class.getClassLoader().getParent());
        Class<?> c1 = loader1.loadClass("com.adee.java.classloader.Person");
        Method m = c1.getDeclaredMethod("newSubject", null);
        Object obj = m.invoke(null);
        // com.adee.java.classloader.MyClassLoader@37a71e93
        System.out.println("obj.getClass().getClassLoader(): " + obj.getClass().getClassLoader());
        // jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
        System.out.println("contextClassLoader:" + Thread.currentThread().getContextClassLoader());
    }
    // 测试不同classLoader实例加载相同类，得到不同的class对象，且这两个class对象的静态变量是各自独立的
    public static void test1() throws Exception{
        // jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
        System.out.println("应用类加载器：" + ClassLoaderTest.class.getClassLoader());
        MyClassLoader loader1 = new MyClassLoader(ClassLoaderTest.class.getClassLoader().getParent());
        MyClassLoader loader2 = new MyClassLoader(ClassLoaderTest.class.getClassLoader().getParent());
        Class<?> c1 = loader1.loadClass("com.adee.java.classloader.Person");
        Class<?> c2 = loader2.loadClass("com.adee.java.classloader.Person");
        System.out.println("c1 == c2: " + (c1 == c2)); // false
        c1.getDeclaredField("type").set(null, "aaa");
        c2.getDeclaredField("type").set(null, "bbb");
        System.out.println(c1.getDeclaredField("type").get(null)); // aaa
        System.out.println(c2.getDeclaredField("type").get(null)); // bbb
    }
    public static void test2() throws Exception{
        MyClassLoader mcl = new MyClassLoader(ClassLoaderTest.class.getClassLoader().getParent());
        Class<?> aClass = mcl.loadClass("com.adee.java.classloader.ClassLoaderTest");
        // aClass.getDeclaredMethod("main1").invoke(null);
        // 类ClassLoaderTest使用MyClassLoader类加载器加载的，则调用该类中的代码时，默认类加载器也是ClassLoaderTest
        Constructor<?> constructor = aClass.getConstructor();
        Object obj = constructor.newInstance();
        aClass.getDeclaredMethod("main1").invoke(obj);
    }
    public static void test3() throws Exception{
        // 将自定义类加载器MyClassLoader的父类加载器设置为ExtClassLoader
        MyClassLoader mcl = new MyClassLoader(ClassLoaderTest.class.getClassLoader().getParent());
//        File clazzFile = mcl.getFile("com.adee.java.classloader.Person");
//        System.out.println(clazzFile.getAbsolutePath());
//        byte[] bytes = mcl.getClassBytes(clazzFile);
        Class clazz = Class.forName("com.adee.java.classloader.Person", false, mcl);
        Object obj = clazz.getConstructor().newInstance();
        System.out.println(obj);
        System.out.println("obj.getClass().getClassLoader():" + obj.getClass().getClassLoader());
//        Subject subject = ((Person) obj).getSubject();
//        System.out.println("subject.getClass().getClassLoader(): " + subject.getClass().getClassLoader());
        System.out.println("getName:" + clazz.getDeclaredMethod("getName").invoke(obj));
        Arrays.stream(clazz.getDeclaredMethods()).forEach(m -> {
            System.out.println(m.getName());
        });
        System.out.println("getSubject:" + clazz.getDeclaredMethod("getSubject").invoke(obj));

        System.out.println("getSubject.classLoader:" + clazz.getDeclaredMethod("getSubject").invoke(obj).getClass().getClassLoader());
        ClassLoaderTest t = new ClassLoaderTest();
        System.out.println("t.getClass().getClassLoader():" + t.getClass().getClassLoader());
    }
}

// 自定义类加载器
class MyClassLoader extends ClassLoader {
    public MyClassLoader() {

    }
    public MyClassLoader(ClassLoader parent) {
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = getFile(name);
        if(f == null) throw new ClassNotFoundException();
        try {
            byte[] bytes = getClassBytes(f);
            Class clazz = this.defineClass(name, bytes, 0, bytes.length);
            return clazz;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    public File getFile(String name) {
        String clazz = name.replaceAll("\\.", "/");
        return new File("F:/mycode/java/jdk/target/classes/" + clazz + ".class");
    }

    public byte[] getClassBytes(File file) throws Exception{
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        long size = fc.size();
        if(size > (long)Integer.MAX_VALUE) {
            throw new RuntimeException("class file is too big, size=" + size);
        }
        ByteBuffer by = ByteBuffer.allocate((int)size);
        fc.read(by);
        return by.array();
    }
}
